package se.lnu.axez;

import javax.microedition.khronos.opengles.GL10;

import se.lnu.axez.shaders.Shader;
import se.lnu.axez.shaders.ShaderProgram;
import android.opengl.GLES20;
import android.opengl.GLU;
import android.opengl.GLUtils;
import android.opengl.Matrix;

/**
 * Stores all matrices required by OpenGL for rendering.
 * Takes care of Model-View matrices stack.
 * 
 * @author stan
 * 
 */
public final class RenderMatrices {
	/**
	 * Available size of matrix stack.
	 */
	public static final int STACK_SIZE = 5;

	private static final String TAG = RenderMatrices.class.getName();

	/**
	 * Projection matrix offset.
	 */
	private static final int PROJ_MATRIX_START = 0;
	
	/**
	 * ModelView-Projection matrix offset.
	 */
	private static final int MVP_MATRIX_START = PROJ_MATRIX_START + Constants.GL_MATRIX_SIZE;

	/**
	 * Normal matrix offset. It is located at the end of array before temporary
	 * matrix.
	 */
	private static final int NORMAL_MATRIX_START = MVP_MATRIX_START + Constants.GL_MATRIX_SIZE;
	
	/**
	 * Temporary matrix offset. Temporary matrix is used for intermediate
	 * calculations.
	 */
	private static final int TMP_MATRIX_START = NORMAL_MATRIX_START + Constants.NORMAL_MATRIX_SIZE;
	
	/**
	 * Temporary vector 1 offset. Temporary vector is used for pick ray
	 * calculations.
	 */
	private static final int TMP_VECTOR1_START = TMP_MATRIX_START + Constants.GL_MATRIX_SIZE;

	/**
	 * Temporary vector 2 offset. Temporary vector is used for pick ray
	 * calculations.
	 */
	private static final int TMP_VECTOR2_START = TMP_VECTOR1_START + Constants.GL_VECTOR_SIZE;

	/**
	 * ModelView matrix offset within current stack entry.
	 */
	private static final int MV_MATRIX_STACK_START = TMP_VECTOR2_START + Constants.GL_VECTOR_SIZE;

	/**
	 * Array containing all stack matrices.
	 * It is structured in following fashion:
	 * <pre>
	 * ======================= Common matrices =========================|===== Stack of MV matrices =====|
	 * --ROJ--|--MVP--|--NORMAL--|--TMP_MX--|--TMP_VEC_1--|--TMP_VEC_2--|---MV1---|---MV2---|---MV3---|..|
	 * </pre>
	 */
	private final float[] matrices = new float[MV_MATRIX_STACK_START + Constants.GL_MATRIX_SIZE * STACK_SIZE];
	
	/**
	 * Current stack offset.
	 */
	private int modelViewStackOffset = 0;
	
	private boolean mvpCalculated = false;
	
	/**
	 * Switches to the first stack matrices.
	 * @return
	 */
	public RenderMatrices reset() {
		this.modelViewStackOffset = 0;
		return this;
	}
	
	/**
	 * Sets current MV matrix.
	 * 
	 * @param matrix
	 *            - array containing MV matrix.
	 * @param offset
	 *            - offset within array.
	 */
	public void setModelViewMatrix(float[] matrix, int offset) {
		System.arraycopy(matrix, offset, 
				this.matrices, this.currentModelViewMatrixOffset(), 
				Constants.GL_MATRIX_SIZE);
		this.mvpCalculated = false;
	}

	/**
	 * Sets current projection matrix.
	 * 
	 * @param matrix
	 *            - array containing projection matrix.
	 * @param offset
	 *            - offset within array.
	 */
	public void setProjectionMatrix(float[] matrix, int offset) {
		System.arraycopy(matrix, offset, this.matrices, PROJ_MATRIX_START, Constants.GL_MATRIX_SIZE);
		this.mvpCalculated = false;
	}

	/**
	 * Multiplies current MV matrix by provided matrix.
	 * 
	 * @param matrix
	 *            - array containing multiplier matrix.
	 * @param offset
	 *            - offset within array.
	 */
	public void multiplyModelViewMatrix(float[] matrix, int offset) {
		Matrix.multiplyMM(this.matrices, TMP_MATRIX_START, 
				this.matrices, this.currentModelViewMatrixOffset(),
				matrix, offset);
		System.arraycopy(this.matrices, TMP_MATRIX_START, 
				this.matrices, this.currentModelViewMatrixOffset(), 
				Constants.GL_MATRIX_SIZE);
		this.mvpCalculated = false;
	}
	
	/**
	 * Pushes current model-view matrix down the stack copying its values to the
	 * top of the stack.
	 */
	public void pushModelViewMatrix() {
		int oldOffset = this.currentModelViewMatrixOffset();
		this.modelViewStackOffset++;
		int newOffset = this.currentModelViewMatrixOffset();
		System.arraycopy(this.matrices, oldOffset, 
				this.matrices, newOffset, 
				Constants.GL_MATRIX_SIZE);
	}

	/**
	 * Pops model-view matrices stack by one level.
	 */
	public void popModelViewMatrix() {
		this.modelViewStackOffset--;
	}
	
	/**
	 * Applies current matrices to {@link ShaderProgram} by setting program
	 * uniforms.
	 * 
	 * @param program
	 *            - {@link ShaderProgram} to set matrices for.
	 */
	public void applyMatrices(ShaderProgram program) {
		if (program.isFeatureRequested(Shader.SHADER_FEATURE_UNI_MVP_MATRIX)) {
			this.calculateMVPMatrix();
			int uniformID = program.getUniformID(Shader.UNI_NAME_MVP_MATRIX);
			GLES20.glUniformMatrix4fv(uniformID, 1, false, this.matrices, MVP_MATRIX_START);
			Utils.checkGlError(TAG, "glUniformMatrix4fv");
		}
		
		if (program.isFeatureRequested(Shader.SHADER_FEATURE_UNI_MV_MATRIX)) {
			int uniformID = program.getUniformID(Shader.UNI_NAME_MV_MATRIX);
			GLES20.glUniformMatrix4fv(uniformID, 1, false, this.matrices, this.currentModelViewMatrixOffset());
			Utils.checkGlError(TAG, "glUniformMatrix4fv");
		}
		
		if (program.isFeatureRequested(Shader.SHADER_FEATURE_UNI_NORMAL_MATRIX)) {
			this.calculateNormalMatrix();
			int uniformID = program.getUniformID(Shader.UNI_NAME_NORMAL_MATRIX);
			
			// TODO: consider OpenGL transpose usage
			GLES20.glUniformMatrix3fv(uniformID, 1, false, this.matrices, NORMAL_MATRIX_START);
			Utils.checkGlError(TAG, "glUniformMatrix3fv");
		}
	}
	
	private void calculateMVPMatrix() {
		if (this.mvpCalculated) {
			return;
		}
		
		Matrix.multiplyMM(this.matrices, MVP_MATRIX_START, 
				this.matrices, PROJ_MATRIX_START, 
				this.matrices, this.currentModelViewMatrixOffset());
		this.mvpCalculated = true;
	}
	
	private void calculateNormalMatrix() {
		// TODO: reconsider this implementation with Matrix.invertM usage
		
		// normal_matrix = transpose(inverse(mat3(model_view_matrix)))
		float[] m = this.matrices;
		int mvOffset = this.currentModelViewMatrixOffset();
		
		float determinant = m[mvOffset + 0]*(m[mvOffset + 5]*m[mvOffset + 10] - m[mvOffset + 6]*m[mvOffset + 9]) + 
							m[mvOffset + 1]*(m[mvOffset + 6]*m[mvOffset + 8] - m[mvOffset + 10]*m[mvOffset + 4]) + 
							m[mvOffset + 2]*(m[mvOffset + 4]*m[mvOffset + 9] - m[mvOffset + 5]*m[mvOffset + 8]);
		
		if (determinant == 0) {
			throw new RuntimeException("Cannot calculate inverse model-view matrix.");
		}
		
		this.matrices[NORMAL_MATRIX_START + 0] = (m[mvOffset + 5]*m[mvOffset + 10] - m[mvOffset + 6]*m[mvOffset + 9])/determinant;
		this.matrices[NORMAL_MATRIX_START + 1] = (m[mvOffset + 6]*m[mvOffset + 8] - m[mvOffset + 4]*m[mvOffset + 10])/determinant;
		this.matrices[NORMAL_MATRIX_START + 2] = (m[mvOffset + 4]*m[mvOffset + 9] - m[mvOffset + 5]*m[mvOffset + 8])/determinant;
		
		this.matrices[NORMAL_MATRIX_START + 3] = (m[mvOffset + 2]*m[mvOffset + 9] - m[mvOffset + 1]*m[mvOffset + 10])/determinant;
		this.matrices[NORMAL_MATRIX_START + 4] = (m[mvOffset + 0]*m[mvOffset + 10] - m[mvOffset + 2]*m[mvOffset + 8])/determinant;
		this.matrices[NORMAL_MATRIX_START + 5] = (m[mvOffset + 1]*m[mvOffset + 8] - m[mvOffset + 0]*m[mvOffset + 9])/determinant;

		this.matrices[NORMAL_MATRIX_START + 6] = (m[mvOffset + 1]*m[mvOffset + 6] - m[mvOffset + 2]*m[mvOffset + 5])/determinant;
		this.matrices[NORMAL_MATRIX_START + 7] = (m[mvOffset + 2]*m[mvOffset + 4] - m[mvOffset + 0]*m[mvOffset + 6])/determinant;
		this.matrices[NORMAL_MATRIX_START + 8] = (m[mvOffset + 0]*m[mvOffset + 5] - m[mvOffset + 1]*m[mvOffset + 4])/determinant;
	}
	
	private int currentModelViewMatrixOffset() {
		return MV_MATRIX_STACK_START + this.modelViewStackOffset * Constants.GL_MATRIX_SIZE;
	}
	
	public boolean getPickRayCoordinates(float[] cameraPickCoordinates, int cameraPickOffset, float[] result, int resultOffset) {
		this.calculateMVPMatrix();
		
		// store inverted MVP matrix in temporary region
		if (!Matrix.invertM(this.matrices, TMP_MATRIX_START, this.matrices, MVP_MATRIX_START)) {
			return false;
		}
		
		// set common pick coordinates
		this.matrices[TMP_VECTOR1_START + 0] = cameraPickCoordinates[cameraPickOffset];
		this.matrices[TMP_VECTOR1_START + 1] = cameraPickCoordinates[cameraPickOffset + 1];
		this.matrices[TMP_VECTOR1_START + 3] = 1f;

		float w;
		
		// unproject near coordinates
		this.matrices[TMP_VECTOR1_START + 2] = -1f;
		Matrix.multiplyMV(this.matrices, TMP_VECTOR2_START, this.matrices, TMP_MATRIX_START, this.matrices, TMP_VECTOR1_START);
		w = this.matrices[TMP_VECTOR2_START + 3];
		if (w == 0f) {
			return false;
		}
		
		result[resultOffset + 0] = this.matrices[TMP_VECTOR2_START + 0] / w;
		result[resultOffset + 1] = this.matrices[TMP_VECTOR2_START + 1] / w;
		result[resultOffset + 2] = this.matrices[TMP_VECTOR2_START + 2] / w;
		result[resultOffset + 3] = this.matrices[TMP_VECTOR2_START + 3] / w;
		
		// unproject far coordinates
		this.matrices[TMP_VECTOR1_START + 2] = 1f;
		Matrix.multiplyMV(this.matrices, TMP_VECTOR2_START, this.matrices, TMP_MATRIX_START, this.matrices, TMP_VECTOR1_START);
		w = this.matrices[TMP_VECTOR2_START + 3];
		if (w == 0f) {
			return false;
		}
		
		result[resultOffset + 4] = this.matrices[TMP_VECTOR2_START + 0] / w;
		result[resultOffset + 5] = this.matrices[TMP_VECTOR2_START + 1] / w;
		result[resultOffset + 6] = this.matrices[TMP_VECTOR2_START + 2] / w;
		result[resultOffset + 7] = this.matrices[TMP_VECTOR2_START + 3] / w;
		
		return true;
	}
}
